feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953) - #963
Conversation
… heard, and the five keys upstream refuses that we dropped (#672, #953) Developer directive: parity on what upstream supports ("we want to be a good reference"), usage docs, and in those docs the weights — then, mid-flight, "we should allow to load only the music model" and "we need to have an e2e test working". UPSTREAM WAS ENUMERATED FIELD BY FIELD, not summarized: SGLang-Omni `748a0b43` `models/minimax_music3/` and diffusers `c6da9936` `modular_pipelines/minimax_music3/`, recorded with `file:line` in spec section 10.1 so the next reader re-derives nothing. --model IS NOW OPTIONAL WHEN --speech-model IS GIVEN. Serving a 28.5 GB music model also forced loading an unrelated text model, and on this box the smallest text checkpoint is 35B — so the recipe this project documented was effectively unrunnable, while upstream's own is `sgl-omni serve --model MiniMaxAI/MiniMax-Music3` with no text tower anywhere (`config.py:29-63`: three stages, no chat LLM). `--speech-model` alone now loads only the speech engine and registers only `/v1/audio/speech`. It is the THIRD instance of a shape already in this file — a pooling checkpoint serves `/v1/embeddings` alone, a Parakeet checkpoint serves `/v1/audio/transcriptions` alone — and it mirrors vLLM's task-conditional registration (`api_server.py:255-265`). IT IS ADDITIVE AND THAT IS PROVED, NOT ARGUED. The only case whose verdict changes is BOTH flags absent, which was an error and remains one, with a message that now names both ways to satisfy it. `--model` alone and `--model` + `--speech-model` take byte-identical paths. The route table is gated in BOTH DIRECTIONS OVER A REAL SOCKET, because a handler-dispatch test cannot see route registration at all: with no synthesizer `/v1/audio/speech` is a 404 from the route table with no envelope leaked, and on a speech-only server `/v1/completions` and `/v1/chat/completions` are 404 while `/v1/audio/speech` returns audio/wav. THE MUSIC FAMILY WAS THE ONE GENERATIVE FAMILY IN THIS TREE WITH NO EXAMPLE. `examples/minimax_music3_gen` is a THIN client of `include/vllm.h` and nothing else, like `parakeet-transcribe` and `vllm-cli`: `vllm_speech_engine_load`, the three interrogations of the handle, `vllm_synthesize`, and the RIFF bytes the result already carries. `--lyrics`/`--description` take `@path` because a `[Verse]` tag inside an argv is easy to mangle, and it reports the DELIVERED length rather than the requested one, since a duration resolves to a whole number of 25 Hz frames. FIVE KEYS UPSTREAM REFUSES BY NAME WERE SILENT HERE (#953, fixed in flow). `temperature`, `top_p`, `top_k`, `repetition_penalty` — `request_builders.py:14-19,109-114` refuses all four, because this model's AR stage has ONE sampler, a fixed top-50 draw (`encoders.py:48,94-103`): there is no temperature to set and no nucleus branch to widen. And `max_new_tokens`, which is upstream's LENGTH spelling in 25 Hz FRAMES rather than seconds (`request_builders.py:56-68`), so a 250-frame request silently became the family's 60 s default. That is the #925 class exactly, in the same file that already carries #925's refusal one paragraph above. THE E2E GATE NO LONGER REPORTS A SKIP WEARING A PASS. It read 5 cases / 5 passed with `assertions: 0` whenever the checkpoint was absent — five green case names over an empty run, the same shape that fooled this project on `test_qwen3_paged_engine`. The file is split: a CHECKPOINT-FREE half runs unconditionally in CI (the request contract on the exact body the real case posts, both ceilings, and the speech-only route table over a real socket with a stub synthesizer), and the env-gated half now drives the REAL engine over a REAL SOCKET against the music-only server shape rather than calling `handle_audio_speech`. A coverage-report case prints which arms ran, every run. Its assertion is deliberately NOT a cross-case counter — `-tc` runs one case alone, the counter is legitimately zero, and a gate that reds for the way it was invoked is a gate somebody deletes; it asserts a cheap fact about the checkpoint itself instead (44100 Hz, hop 512, vocab 200000, 8 codebooks, read from the component config.json files). no env vars 9 cases / 37 assertions (was 5 / 0) test_speech_api 6 cases / 67 assertions (was 5 / 47) openai_api_server 62 cases / 733 assertions (was 61 / 719) server flag cases 7 passed (was 4) THE WEIGHTS ARE DOCUMENTED, per `.agents/porting-a-model.md` section 2.1. `docs/USAGE.md` carries component-by-component tables for the diffusers arm at `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec` — 28.5 GB resident (28 517 617 303 B, MEASURED) out of a 57.4 GB repository, and why the two differ — the native `.pth` arm we refuse and SGLang-Omni serves, the one implemented GGUF Q4_K artifact with its sha256, and the FOURTEEN THIRD-PARTY quantized repositories in five formats, each marked refused and each marked third-party. The revision is VERIFIED rather than copied: `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to `83179c5e…a202c2a4d`, which is that revision's own LFS record for the file. OWED AND NAMED rather than left to be discovered: the non-`wav` response formats (no encoder vendored — and note upstream DOWNMIXES TO MONO for every non-wav format, `client/audio.py:328-334`), request batching and `/v1/audio/speech/batch` (SGLang runs continuous batching at 16 with TWO engine rows per request for the CFG twin, `engine_builder.py:74-77`), and the 32 kHz delivery resample. Streaming is a PERMANENT refusal rather than a debt: neither upstream arm has it (`supports_streaming_vocoder=False`). One place we are AHEAD of both arms: `guidance_scale` is a real per-request control here, where diffusers freezes it at 1.7 into the guider component (`denoise.py:180`) and SGLang exposes it only as a serve-time knob. Four mutations run, all four fire: the sampling refusal neutered (5 assertions red), the `max_new_tokens` refusal neutered (2 red), `--model` made mandatory again (2 ctest cases red), and the generate routes registered unconditionally (3 cases / 6 assertions red in the api-server suite and 1 / 2 in the e2e suite). Sources restored and verified sha256-identical. Issue: #672 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…t takes RUN_SERIAL like the other two that do (#672) The speech-only route-table cases added in the previous commit bind a real cpp-httplib server on an ephemeral port. `test_openai_api_server` and `test_openai_conformance` already carry `RUN_SERIAL` for exactly that reason, recorded three lines below in this file: under a saturated `ctest -jN` the server's accept thread starves and the client sees a connection failure rather than the defect it was looking for. A socket test that flakes teaches people to re-run it, which is how a real red gets waved through. Issue: #672 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…hy it is not committed (#672) The e2e gate's own artifact is 0.07 s — the shortest request that still enters every stage — which nobody can listen to. `minimax-music3-gen --duration 2.0 --steps 2 --seed 7` produced 88 064 frames per channel of 44100 Hz stereo in 3286 s of wall clock on a 20-core x86 CPU box at load average 7-150. Verified by RE-READING the RIFF file rather than trusting the generator's own report: RMS 0.03169, peak 0.97437 full-scale with 0 CLIPPED samples, 175 858 of 176 128 int16 samples non-zero, and 84 073 of 88 064 positions differing between left and right — so the 128 latent channels are folded into two streams of 64 rather than interleaved, which is one of the four ways a well-formed WAV can still not be a song. ITS SAMPLES ARE COMPARED TO NOTHING, and that is structural rather than an omission. Spec section 5 withdrew the token gate because upstream's autoregressive stage has no greedy path, and a request's waveform can never equal `waveform.npy` in any case: both the codes and the initial latents are seeded random draws. The clip demonstrates that the pipeline RUNS and emits a well-formed, non-silent, non-clipped, genuinely stereo signal. It says nothing about whether the music is right; the per-stage gates are what say that. No speed claim is made or implied — the acoustic half is upstream's own fp32 and the depth decoder and DiT are scalar host loops by construction. IT IS NOT COMMITTED, and the reason is a CHECKER rather than a preference. `scripts/check-pr-size.py` classifies every repository path: `ASSET` takes `assets/*.{png,svg}`, `BENCH_EVIDENCE` takes `benchmarks/{demo,media}/*.{json,png,gif,mp4,log}`, and neither takes a `.wav`. The only classified home for one is under `tests/`, where a file compared to nothing would sit beside the oracle goldens and imply it was one — which `test_minimax_music3_e2e_real.cpp` already refuses for its own artifact in as many words. Widening either pattern would be widening a checker's scope to make a change pass, which AGENTS.md forbids without its own spec and red-before evidence, and one demo clip does not justify one. Regenerating it is a single command, and that command is now in the docs. Issue: #672 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…kticks (#672) The weights table's last component row names the checkpoint set's ROOT — the three files that sit beside the six component directories. It was written as an empty inline-code span, which renders as two backticks and a bracketed word rather than as a name, so the one row a reader needs in order to know the root carries files at all was the least legible in the table. Issue: #672 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…erence between them is visible (#672) Spec section 10.3 said the suite was split into a checkpoint-free half and an env-gated half. It did not say what each arm actually examines, which leaves the reader to take "split" on trust — and the whole point of the split was that a gate which cannot say HOW MANY things it examined has not reported. Measured on this box: 9 cases / 37 assertions with no env vars (it was 5 / 0), 9 / 86 with the checkpoint alone, and 9 / 582 with the checkpoint and VLLM_CPP_MUSIC3_DIT=1. The counters the coverage-report case prints go with them: 0, 3 and 5 checkpoint arms respectively. The full arm's own numbers are recorded too, because they are the first ones taken over a REAL SOCKET against the real 28.5 GB engine rather than a stub: 200 audio/wav, 12332 bytes in 518.0 s of wall clock, 2 AR frames to 6 latent frames to 3072 samples per channel, 6144 int16 samples all non-zero with 0 clipped and 2818 of 3072 positions differing between left and right — and /v1/completions and /v1/chat/completions both 404 from the route table, which is the music-only claim proved over the wire rather than through a handler call. Issue: #672 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
FOLLOWING_AGENTS_PROTOCOL Keeps the branch current before review. No content of this row changes; the incoming commit is the weight-offload docs fix (#958). Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
FOLLOWING_AGENTS_PROTOCOL Keeps the branch current before review. The incoming commit is the LTX-2.5 token-append phase loop (#930); it touches no MiniMax-Music3 surface, and the keyed records it edits (issue-index, FEATURES, USAGE) merge on disjoint keys. Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…W6's own variable shadow (#965, #672) `windows-msvc-cpu` and `windows-msvc-vulkan` fail on every open pull request: server_main.cpp(1315,55): error C2220: the following warning is treated as an error server_main.cpp(1315,55): warning C4456: declaration of 'loaded' hides previous local declaration That is W6's own speech-attach block declaring `loaded` inside the scope of the text engine's `loaded` at `:1025`. It is the ONLY warning in the job, it has been on `main` since W6 landed, and it is fixed here by renaming the inner declaration. Nothing is suppressed and no detector is weakened. WHAT FOUND IT WAS THE MATCHED-ARM CHECK, NOT THE LABEL, and that is the part worth recording. Both jobs are habitually red and habitually attributed to #645 — which is the `M_PI` portability regression in three LTX2 sources: different file, different detector, different failure. A second cause sitting behind a known-red name is invisible for exactly as long as nobody reads the log. Three unrelated open pull requests that touch no speech surface — #956, #950 and #939 — fail with the identical C4456, which is what separates "pre-existing" from "mine". `main` carries no baseline because `windows-msvc-*` are PR-only (#584), so the failure presents to each author in turn as a red their own diff caused. Verified after the rename: 7 of 7 server ctest cases pass, `test_openai_api_server` is 62 cases / 733 assertions, and `vllm-server --speech-model <dir>` with no `--model` starts and serves for real: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') Issue: #965 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
|
Superseded by #966 — same tree, one extra
The right repair was for that commit to have carried its documentation, and it now does: #966 documents what a successful music-only start prints ( Verified before pushing this time: |
… heard, and the five keys upstream refuses that we dropped (#672, #953, #965) (#966) feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953) Developer directive: parity on what upstream supports — "we want to be a good reference" — usage docs for MiniMax-Music3, and in those docs the weights. Then, mid-flight: "we should allow to load only the music model" and "we need to have an e2e test working". FOLLOWING_AGENTS_PROTOCOL ## The upstream surface, enumerated rather than summarized SGLang-Omni `748a0b43` `models/minimax_music3/` and diffusers `c6da9936` `modular_pipelines/minimax_music3/`, read field by field and recorded with `file:line` in spec §10.1 so the next reader re-derives nothing. **Closed here:** the music-only server, the missing example, and five refusals. **Owed and named:** the non-`wav` response formats, request batching and `/v1/audio/speech/batch`, the 32 kHz delivery resample. **Permanently refused rather than owed:** streaming — neither upstream arm has it (`supports_streaming_vocoder=False`). **One place we are ahead of both arms:** `guidance_scale` is a real per-request control here, where diffusers freezes it at 1.7 into the guider component (`denoise.py:180`) and SGLang exposes it only as a serve-time knob. ## `--model` is optional when `--speech-model` is given Serving a 28.5 GB music model also forced loading an unrelated text model, and on this box the smallest text checkpoint is 35B — so the recipe this project documented was effectively unrunnable. Upstream's own is `sgl-omni serve --model MiniMaxAI/MiniMax-Music3`, no text tower anywhere. vllm-server --speech-model /path/to/minimax-music3 Third instance of a shape already in `server_main.cpp`: a pooling checkpoint serves `/v1/embeddings` alone, a Parakeet checkpoint serves `/v1/audio/transcriptions` alone. It mirrors vLLM's task-conditional registration (`api_server.py:255-265`). **Additive, and proved rather than argued.** The only case whose verdict changes is BOTH flags absent, which was an error and remains one with a message naming both options. The route table is gated in both directions over a real socket, because a handler-dispatch test cannot see route registration at all. ## The example the music family did not have `examples/minimax_music3_gen` — a thin client of `include/vllm.h` and nothing else, like `parakeet-transcribe` and `vllm-cli`. Hearing this model previously needed a running server plus a `curl`, or a C ABI caller nobody had written. ## Five keys upstream refuses by name were SILENT here (#953) `temperature`, `top_p`, `top_k`, `repetition_penalty` — refused upstream at `request_builders.py:14-19,109-114`, because this model's autoregressive stage has ONE sampler, a fixed top-50 draw (`encoders.py:48,94-103`). And `max_new_tokens`, upstream's LENGTH spelling in 25 Hz frames rather than seconds (`request_builders.py:56-68`), so a 250-frame request silently became the family's 60 s default. That is the #925 class exactly, in the same file that already carries #925's refusal one paragraph above. Fixed in flow. ## The e2e gate no longer reports a skip wearing a pass It read 5 cases / 5 passed with **`assertions: 0`** whenever the checkpoint was absent — the same shape that fooled this project on `test_qwen3_paged_engine`. Split into a checkpoint-free half that runs unconditionally in CI (request contract, both ceilings, the speech-only route table over a real socket with a stub synthesizer) and the env-gated half, whose HTTP case now drives the real engine over a real socket against the music-only server shape. A coverage-report case prints which arms ran, every run. The full arm was run: `POST /v1/audio/speech -> 200 audio/wav, 12332 bytes in 518 s wall`, 2 AR frames -> 6 latent frames -> 3072 samples/channel, 6144 int16 samples all non-zero, 0 clipped, 2818 of 3072 positions differing between L and R, and `/v1/completions` + `/v1/chat/completions` both 404 from the route table. `checkpoint_arms_run=5`. | arm | cases | assertions | |---|---|---| | `test_minimax_music3_e2e_real`, no env vars | 9 | 37 (was 5 / **0**) | | `test_minimax_music3_e2e_real`, checkpoint only | 9 | 86 | | `test_minimax_music3_e2e_real`, checkpoint + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | | `test_speech_api` | 6 | 67 | | `test_openai_api_server` | 62 | 733 | | `test_openai_conformance` | 23 | 252 | | `test_minimax_h3` (unchanged) | 79 | 57395 | | server flag ctest cases | 7 passed | | ## The weights are documented (porting-a-model.md §2.1) `docs/USAGE.md` gains component-by-component tables: the diffusers arm at `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, **28.5 GB resident** (28 517 617 303 B, measured) out of a 57.4 GB repository and why they differ; the native `.pth` arm we refuse and SGLang-Omni serves; the one implemented GGUF Q4_K artifact with its sha256; and the fourteen third-party quantized repositories in five formats, each marked refused. The revision is verified rather than copied — `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to that revision's own LFS record. ## A sample a human can hear 2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS 0.03169, peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions differing between left and right. **Its samples are compared to nothing** — §5 withdrew the token gate — so it shows the pipeline runs, not that the music is right. It is not committed: `check-pr-size.py` classifies every path and none takes a `.wav` outside `tests/`, where a file compared to nothing would sit beside the goldens and imply it was one. ## The four asks, answered directly **1. Music-only server.** `vllm-server --speech-model <dir>` with NO `--model` starts and serves, observed live rather than inferred: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') `--model` alone and `--model` + `--speech-model` are **byte-identical in behaviour**. The whole change is one new early branch, `if (args.model_dir.empty())`, which loads the speech engine and `return 0`s before reaching a single line of the existing path; nothing downstream of it was touched. The only case whose verdict changes is BOTH flags absent, which was an error and remains one. Server suites: **7 of 7** ctest cases (4 pre-existing + 3 new — neither flag is still an error and now names both options; `--speech-model` alone reaches the speech LOAD; `--speech-family` alone still demands a checkpoint), `test_openai_api_server` **62 cases / 733 assertions** (+1 case / +24 assertions, the speech-only route table over a real socket), `test_openai_conformance` **23 / 252** unchanged. **2. e2e, three arms.** What a bare CI run executes unconditionally: the request contract on the exact body the real case posts, the near-miss and sampling refusals, the duration arithmetic including both ceilings, and the speech-only route table over a real socket with a stub synthesizer. What stays env-gated: everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms behind `VLLM_CPP_MUSIC3_DIT`. | arm | cases | assertions | checkpoint arms run | |---|---|---|---| | no env vars (what CI runs) | 9 | **37** | 0 — was 5 cases / **0 assertions** | | `VLLM_CPP_MUSIC3_CHECKPOINT` | 9 | **86** | 3 | | + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | 5 | **3. The five keys.** All five were **accepted and silently dropped**; all five are **now refused by name**. | key | upstream anchor | why it cannot be honoured | |---|---|---| | `temperature` | `request_builders.py:14-19,109-114` | the AR stage's only sampler is a fixed top-50 draw, `encoders.py:48,94-103` | | `top_p` | same | no nucleus branch exists | | `top_k` | same | `_AR_SAMPLING_TOP_K` is a module constant of 50 | | `repetition_penalty` | same | no penalty is applied anywhere in the loop | | `max_new_tokens` | `request_builders.py:56-68`, `constants.py:4-5` | upstream's LENGTH, in 25 Hz frames not seconds; the refusal names `audio_duration` and the /25 conversion | **4. The weights table** (`docs/USAGE.md`, "MiniMax-Music3: the exact weights"). It carries: repo **and** revision — `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, verified rather than copied, since `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to `83179c5e…a202c2a4d`, that revision's own LFS record; the Q4_K artifact's sha256 `4c5d41b2…c70cbdd0` at revision `c36aaeed…` with its exact byte count; **28.5 GB resident (28 517 617 303 B, measured) versus 57.4 GB repository**, with the reason they differ; the refused native `.pth` arm (`qwen_7B/`, `flowmatching_vae.pth`, `dav.pth`) and that SGLang-Omni serves it; and all fourteen community quant repositories across five formats, each marked refused and each marked **third-party** rather than first-party. This is the first application of `.agents/porting-a-model.md` §2.1 (landing as #951). ## Two reds stacked behind one habitually-red job name `windows-msvc-cpu`/`windows-msvc-vulkan` are usually attributed to #645 (`M_PI` in three LTX2 sources). **Neither of the two causes here was #645**, and the first hid the second. **#965, fixed in flow.** `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — W6's own speech-attach block declaring `loaded` inside the text engine's `loaded`. The only warning in the job, on `main` since W6 landed. Matched arm: #956, #950 and #939, none touching the speech surface, fail identically. Renamed; nothing suppressed. **#968, filed and NOT fixed here.** With the shadow gone the same jobs failed again on `C4244: conversion from 'const double' to 'float'`, raised inside MSVC's `<vector>` from `ltx2_video.cpp:203,214` — two narrowing `positions.assign` calls that `c7cb59fbb` (#964) landed on `main` while this row was in flight. **This branch touches zero LTX2 files.** The matched arm splits exactly on the merge base: #966 and #951 (on `c7cb59fbb`) hit it, #967/#956/ #950/#939/#938 (before it) do not. It is deliberately left to the LTX-2.5 lane — #964's own comment reasons that "double -> float -> double reproduces the bits", so a silencing cast is a claim about that reasoning rather than a formatting fix. **The finding, which outlives both:** a known-red list tells you a job is often red. It never tells you that today's red is the same one. Only reading the log does — and here it took two readings, because removing the first cause is what made the second visible. <!-- kept for the record --> ### The first of the two, in detail (#965) `windows-msvc-cpu`/`windows-msvc-vulkan` failed here, and they are **not** #645 (`M_PI` in three LTX2 sources). They were W6's own `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — the only warning in the job, on `main` since W6 landed. The matched-arm check is what separated it from my diff: #956, #950 and #939, all touching no speech surface, fail identically. `main` has no baseline because `windows-msvc-*` are PR-only (#584), so it presents to each author in turn as their own red. Filed and fixed in flow by renaming the inner declaration; nothing suppressed. ## Mutations Four run, four fire: sampling refusal neutered (5 assertions red), `max_new_tokens` refusal neutered (2 red), `--model` made mandatory again (2 ctest cases red), generate routes registered unconditionally (3 cases / 6 assertions red in the api-server suite, 1 / 2 in the e2e suite). Sources restored and verified sha256-identical. Supersedes #954 (untrailered merge commits) and #963 (a `server_main.cpp` commit that owed `docs/USAGE.md` under the per-commit documentation checkpoint). Same tree, linear history, every commit green on `check-doc-checkpoint`, `check-commit-trailers` and `check-commit-style` locally before pushing. Every source file is byte-identical to the one built and gated. Issue: #672 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953)
Developer directive: parity on what upstream supports — "we want to be a good
reference" — usage docs for MiniMax-Music3, and in those docs the weights. Then,
mid-flight: "we should allow to load only the music model" and "we need to have
an e2e test working".
FOLLOWING_AGENTS_PROTOCOL
The upstream surface, enumerated rather than summarized
SGLang-Omni
748a0b43models/minimax_music3/and diffusersc6da9936modular_pipelines/minimax_music3/, read field by field and recorded withfile:linein spec §10.1 so the next reader re-derives nothing.Closed here: the music-only server, the missing example, and five refusals.
Owed and named: the non-
wavresponse formats, request batching and/v1/audio/speech/batch, the 32 kHz delivery resample.Permanently refused rather than owed: streaming — neither upstream arm has
it (
supports_streaming_vocoder=False).One place we are ahead of both arms:
guidance_scaleis a real per-requestcontrol here, where diffusers freezes it at 1.7 into the guider component
(
denoise.py:180) and SGLang exposes it only as a serve-time knob.--modelis optional when--speech-modelis givenServing a 28.5 GB music model also forced loading an unrelated text model, and
on this box the smallest text checkpoint is 35B — so the recipe this project
documented was effectively unrunnable. Upstream's own is
sgl-omni serve --model MiniMaxAI/MiniMax-Music3, no text tower anywhere.Third instance of a shape already in
server_main.cpp: a pooling checkpointserves
/v1/embeddingsalone, a Parakeet checkpoint serves/v1/audio/transcriptionsalone. It mirrors vLLM's task-conditionalregistration (
api_server.py:255-265).Additive, and proved rather than argued. The only case whose verdict changes
is BOTH flags absent, which was an error and remains one with a message naming
both options. The route table is gated in both directions over a real socket,
because a handler-dispatch test cannot see route registration at all.
The example the music family did not have
examples/minimax_music3_gen— a thin client ofinclude/vllm.hand nothingelse, like
parakeet-transcribeandvllm-cli. Hearing this model previouslyneeded a running server plus a
curl, or a C ABI caller nobody had written.Five keys upstream refuses by name were SILENT here (#953)
temperature,top_p,top_k,repetition_penalty— refused upstream atrequest_builders.py:14-19,109-114, because this model's autoregressive stagehas ONE sampler, a fixed top-50 draw (
encoders.py:48,94-103). Andmax_new_tokens, upstream's LENGTH spelling in 25 Hz frames rather than seconds(
request_builders.py:56-68), so a 250-frame request silently became thefamily's 60 s default. That is the #925 class exactly, in the same file that
already carries #925's refusal one paragraph above. Fixed in flow.
The e2e gate no longer reports a skip wearing a pass
It read 5 cases / 5 passed with
assertions: 0whenever the checkpoint wasabsent — the same shape that fooled this project on
test_qwen3_paged_engine.Split into a checkpoint-free half that runs unconditionally in CI (request
contract, both ceilings, the speech-only route table over a real socket with a
stub synthesizer) and the env-gated half, whose HTTP case now drives the real
engine over a real socket against the music-only server shape. A coverage-report
case prints which arms ran, every run.
The full arm was run:
POST /v1/audio/speech -> 200 audio/wav, 12332 bytes in 518 s wall, 2 AR frames -> 6 latent frames -> 3072 samples/channel, 6144 int16samples all non-zero, 0 clipped, 2818 of 3072 positions differing between L and
R, and
/v1/completions+/v1/chat/completionsboth 404 from the route table.checkpoint_arms_run=5.test_minimax_music3_e2e_real, no env varstest_minimax_music3_e2e_real, checkpoint onlytest_minimax_music3_e2e_real, checkpoint +VLLM_CPP_MUSIC3_DIT=1test_speech_apitest_openai_api_servertest_openai_conformancetest_minimax_h3(unchanged)The weights are documented (porting-a-model.md §2.1)
docs/USAGE.mdgains component-by-component tables: the diffusers arm atMiniMaxAI/MiniMax-Music3@fbdf52fbaaca799592917417eb05f1899f1255ec, 28.5 GBresident (28 517 617 303 B, measured) out of a 57.4 GB repository and why they
differ; the native
.ptharm we refuse and SGLang-Omni serves; the oneimplemented GGUF Q4_K artifact with its sha256; and the fourteen third-party
quantized repositories in five formats, each marked refused. The revision is
verified rather than copied —
condition_encoder/diffusion_pytorch_model.safetensorson disk hashes to that revision's own LFS record.
A sample a human can hear
2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS 0.03169,
peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions differing
between left and right. Its samples are compared to nothing — §5 withdrew
the token gate — so it shows the pipeline runs, not that the music is right. It
is not committed:
check-pr-size.pyclassifies every path and none takes a.wavoutsidetests/, where a file compared to nothing would sit beside thegoldens and imply it was one.
The four asks, answered directly
1. Music-only server.
vllm-server --speech-model <dir>with NO--modelstarts and serves, observed live rather than inferred:
--modelalone and--model+--speech-modelare byte-identical inbehaviour. The whole change is one new early branch,
if (args.model_dir.empty()),which loads the speech engine and
return 0s before reaching a single line ofthe existing path; nothing downstream of it was touched. The only case whose
verdict changes is BOTH flags absent, which was an error and remains one.
Server suites: 7 of 7 ctest cases (4 pre-existing + 3 new — neither flag is
still an error and now names both options;
--speech-modelalone reaches thespeech LOAD;
--speech-familyalone still demands a checkpoint),test_openai_api_server62 cases / 733 assertions (+1 case / +24 assertions,the speech-only route table over a real socket),
test_openai_conformance23 / 252 unchanged.
2. e2e, three arms. What a bare CI run executes unconditionally: the request
contract on the exact body the real case posts, the near-miss and sampling
refusals, the duration arithmetic including both ceilings, and the speech-only
route table over a real socket with a stub synthesizer. What stays env-gated:
everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms behind
VLLM_CPP_MUSIC3_DIT.VLLM_CPP_MUSIC3_CHECKPOINTVLLM_CPP_MUSIC3_DIT=13. The five keys. All five were accepted and silently dropped; all five
are now refused by name.
temperaturerequest_builders.py:14-19,109-114encoders.py:48,94-103top_ptop_k_AR_SAMPLING_TOP_Kis a module constant of 50repetition_penaltymax_new_tokensrequest_builders.py:56-68,constants.py:4-5audio_durationand the /25 conversion4. The weights table (
docs/USAGE.md, "MiniMax-Music3: the exact weights").It carries: repo and revision —
MiniMaxAI/MiniMax-Music3@fbdf52fbaaca799592917417eb05f1899f1255ec, verifiedrather than copied, since
condition_encoder/diffusion_pytorch_model.safetensorson disk hashes to
83179c5e…a202c2a4d, that revision's own LFS record; theQ4_K artifact's sha256
4c5d41b2…c70cbdd0at revisionc36aaeed…with its exactbyte count; 28.5 GB resident (28 517 617 303 B, measured) versus 57.4 GB
repository, with the reason they differ; the refused native
.ptharm(
qwen_7B/,flowmatching_vae.pth,dav.pth) and that SGLang-Omni serves it;and all fourteen community quant repositories across five formats, each marked
refused and each marked third-party rather than first-party. This is the
first application of
.agents/porting-a-model.md§2.1 (landing as #951).One red that belonged to nobody (#965)
windows-msvc-cpu/windows-msvc-vulkanfailed here, and they are not #645(
M_PIin three LTX2 sources). They were W6's ownC4456: declaration of 'loaded' hides previous local declarationatserver_main.cpp:1315— the only warning in the job, onmainsince W6 landed.The matched-arm check is what separated it from my diff: #956, #950 and #939, all
touching no speech surface, fail identically.
mainhas no baseline becausewindows-msvc-*are PR-only (#584), so it presents to each author in turn astheir own red. Filed and fixed in flow by renaming the inner declaration; nothing
suppressed.
Mutations
Four run, four fire: sampling refusal neutered (5 assertions red),
max_new_tokensrefusal neutered (2 red),--modelmade mandatory again(2 ctest cases red), generate routes registered unconditionally (3 cases /
6 assertions red in the api-server suite, 1 / 2 in the e2e suite). Sources
restored and verified sha256-identical.
Supersedes #954, which carried two untrailered merge commits; this branch is the
same tree with a linear history.
Issue: #672
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]